//
// Created by guoshunw on 2021/3/12.
//

#include "ThirdPartLibOggAnalyzer.h"


int ThirdPartLibOggAnalyzer::parse() {

    if (nullptr == this->_parsingFile) {
        logError("Parsing file %s is empty", _parsingFile);
        return -1;
    }

    FILE *oggOPUS = fopen(this->_parsingFile, "rb");
    if (nullptr == oggOPUS) {
        return -1;
    }
    ogg_page og;
    ogg_sync_state oy;
#define BLOCK_SIZE 65535  // 64K
    ogg_sync_init(&oy);
    ogg_stream_state os;

    // reset serial no later
    auto ret = ogg_stream_init(&os, -1);
    if (ret < 0) {
        logWarn("Initialize ogg stream failed.");
        return ret;
    }
    // https://cpp.hotexamples.com/examples/-/-/ogg_stream_init/cpp-ogg_stream_init-function-examples.html
    auto pageLimit = 10000L;
    long pageNo = -1L;
    int rate = -1;
    int packets_limit = 3;

    while (true) {

        int result = ogg_sync_pageout(&oy, &og);
        if (-1 == result) {
            logWarn("out of sync and there is a gap in the data");
            ogg_sync_clear(&oy);
            continue;
        }
        if (0 == result) {
            logWarn("There is insufficient data available to complete a packet");

            char *buffer = ogg_sync_buffer(&oy, BLOCK_SIZE);
            auto bytes = fread(buffer, 1, BLOCK_SIZE, oggOPUS);
            if (0 == bytes || pageNo > pageLimit) { // end of file may reached
                logInfo("File end or page number greater than %ld....", pageLimit);
                break;
            }
            logInfo("wrote %ld bytes into ogg bitstream", (long) bytes);
            ogg_sync_wrote(&oy, bytes);
            continue;
        }

        auto packets = ogg_page_packets(&og);
        pageNo = ogg_page_pageno(&og);
        auto serialNo = ogg_page_serialno(&og);
        // success
        logInfo("\n===================================== New page start =================================\n"
                "Page info ==>serial: %x, No. %ld, bos: %d, version: %d, packets: %d, granulepos: %lld",
                serialNo,
                pageNo,
                ogg_page_bos(&og),
                ogg_page_version(&og),
                packets,
                ogg_page_granulepos(&og)
        );

        // process current ogg page
        ogg_stream_reset_serialno(&os, serialNo);
        if (ogg_stream_pagein(&os, &og) < 0) {
            logError("Error reading first page");
            continue;
        }

        // one page include several ogg packets
        // if the page packet is 1, for opus ogg stream, it should be opus id header or opus tags

        // extract the ogg packets
        // only print limit packets for each page
        for (int i = 0; i < min(packets_limit, packets); ++i) {
            ogg_packet op;
            auto r = ogg_stream_packetout(&os, &op);
            if (-1 == r) {
                // if we are out of sync and there is a gap in the data.
                // This is usually a recoverable error and subsequent calls to ogg_stream_packetout are likely to succeed.
                // op has not been updated.
                logWarn("we are out of sync and there is a gap in the data");
                continue;
            } else if (0 == r) {
//                if there is insufficient data available to complete a packet, or on unrecoverable internal error occurred.
//                op has not been updated.
            }

            logInfo("packet info-->bytes: %03ld, bos: %03ld, eos: %03ld, no: %02lld, granulepos: %03lld",
                    op.bytes, op.b_o_s, op.e_o_s, op.packetno, op.granulepos);

            if (1 == packets) {
                // if it is opus head
                OpusHead opusHead;
                ret = opus_head_parse(&opusHead, op.packet, op.bytes);
                if (0 == ret) { // is opus head and parse successful
//                    auto header = reinterpret_cast<OpusIDHeader *> (op.packet);
//                    logInfo("opus head: \n%s", header->toString().c_str());
                    logInfo("opus head:\nVersion: %d, channel: %d, pre_skip: %d, sample_rate: %d, output_gain: %d, channel mapping family: %d, stream_count: %d\n",
                            opusHead.version, opusHead.channel_count, opusHead.pre_skip,
                            opusHead.input_sample_rate, opusHead.output_gain, opusHead.mapping_family,
                            opusHead.stream_count);
                    rate = opusHead.input_sample_rate;
                    continue;
                }
                // if it is opus tags
                OpusTags tags;
                ret = opus_tags_parse(&tags, op.packet, op.bytes);
                if (0 == ret) { // is opus tags packet
                    stringstream o;
                    for (int ci = 0; ci < tags.comments; ++ci) {
                        o << "comment " << ci << ": ";
                        for (int c = 0; c < tags.comment_lengths[ci]; ++c) {
                            o << tags.user_comments[ci][c];
                        }
                    }
                    logInfo("vendor: %s, comments=>%s", tags.vendor, o.str().c_str());
                    continue;
                }

            }
            // they are data packet
//            logInfo("Payload: %s", OggPage::hexDump(reinterpret_cast<const char *>(op.packet), 16, op.bytes).c_str());
            // print ogg packets
            logInfo("Opus packet info: %s", opusPacketToString(op.packet, op.bytes, rate).c_str());
//            putchar('\n');
        }

        if (packets > packets_limit) {
            printf("more packets...\n");
        }
    }
    fclose(oggOPUS);
    return 0;
}

ThirdPartLibOggAnalyzer::ThirdPartLibOggAnalyzer(const char *file) : OggAnalyzer(file) {


}

int ThirdPartLibOggAnalyzer::dumpOpusDataToPcm(const char *outPCMFile) {
    // https://opus-codec.org/docs/opusfile_api-0.7/index.html

    if (nullptr == this->_parsingFile) {
        logError("Parsing file %s is empty", _parsingFile);
        return -1;
    }
    int err;
    const char *oggFile = this->_parsingFile;

    OggOpusFile *opusFile{};
    if (0 == strncmp(oggFile, "file:", 5)
        || 0 == strncmp(oggFile, "http:", 5)
        || 0 == strncmp(oggFile, "https:", 6)
            ) {
        // https://opus-codec.org/docs/opusfile_api-0.7/group__stream__open__close.html#gace8a90b60d492f3488ee3f14a7276ddf
        // how to seek: https://wiki.xiph.org/OpusFAQ
        logInfo("Open url %s", oggFile);
        // server information
        OpusServerInfo info{};
        opus_server_info_init(&info);
        /**
         * For ssl use default ca verification
         * https://www.openssl.org/docs/man1.1.0/man3/SSL_CTX_set_default_verify_paths.html
         *
         * SSL_CTX_set_default_verify_paths() specifies that the default locations from which CA certificates are loaded should be used.
         * There is one default directory and one default file. The default CA certificates directory is called "certs" in the default OpenSSL directory.
         * Alternatively the SSL_CERT_DIR environment variable can be defined to override this location.
         * The default CA certificates file is called "cert.pem" in the default OpenSSL directory.
         * Alternatively the SSL_CERT_FILE environment variable can be defined to override this location.
         *
         * */
        //        opusFile = op_open_url(oggFile, &err, OP_GET_SERVER_INFO(&info), OP_SSL_SKIP_CERTIFICATE_CHECK(1), NULL);
        opusFile = op_open_url(oggFile, &err, OP_GET_SERVER_INFO(&info), NULL);
        if (info.name != NULL) {
            fprintf(stderr, "Station name: %s\n", info.name);
        }
        if (info.description != NULL) {
            fprintf(stderr, "Station description: %s\n", info.description);
        }
        if (info.genre != NULL) {
            fprintf(stderr, "Station genre: %s\n", info.genre);
        }
        if (info.url != NULL) {
            fprintf(stderr, "Station homepage: %s\n", info.url);
        }
        if (info.bitrate_kbps >= 0) {
            fprintf(stderr, "Station bitrate: %u kbps\n",
                    (unsigned) info.bitrate_kbps);
        }
        if (info.is_public >= 0) {
            fprintf(stderr, "%s\n",
                    info.is_public ? "Station is public." : "Station is private.");
        }
        if (info.server != NULL) {
            fprintf(stderr, "Server software: %s\n", info.server);
        }
        if (info.content_type != NULL) {
            fprintf(stderr, "Content-Type: %s\n", info.content_type);
        }
        //        int is_ssl = info.is_ssl;
        opus_server_info_clear(&info);
    } else {
        opusFile = op_open_file(oggFile, &err);
    }

    if (nullptr == opusFile) {
        logError("Open opus file %s error: %d", oggFile, err);
        return -1;
    }


    FILE *outFile = fopen(outPCMFile, "wb");
    if (nullptr == outFile) {
        logError("Creating out file %s failed.", outPCMFile);
        op_free(opusFile);
        return -1;
    }

    logInfo("is seekable: %d", op_seekable(opusFile));
    logInfo("link count: %d", op_link_count(opusFile));
    auto currentLink = op_current_link(opusFile);
    auto serial = op_serialno(opusFile, currentLink);
    logInfo("serial: %x, current link: %d", serial, currentLink);
    auto channel = op_channel_count(opusFile, currentLink);
    logInfo("channel: %d", channel);
    auto rawTotal = op_raw_total(opusFile, currentLink);
    logInfo("raw total: %lld", rawTotal);
    auto pcmTotal = op_pcm_total(opusFile, currentLink);
    logInfo("pcm total: %lld", pcmTotal);
    logInfo("bit rate: %d", op_bitrate(opusFile, currentLink));
    //    logInfo("pcm current pos: %lld", op_pcm_tell(opusFile));

    const int OUTPUT_SAMPLE_RATE = 48000;
    //  It is recommended that this be large enough for at least 120 ms of data at 48 kHz per channel (5760 values per channel).
    int duration = 120; //ms
    int bufSize = (OUTPUT_SAMPLE_RATE / 1000) * duration * channel;
    // the bufSize is the frame size, 5760 per channel
    opus_int16 buf[bufSize];


    logInfo("Writing samples into %s", outPCMFile);
#ifdef WRITE_DETAIL
    auto lastFrameGotTime = chrono::high_resolution_clock::now();
#endif


    while (true) {
        auto numOfSamples = op_read(opusFile, buf, bufSize, &currentLink);
        if (numOfSamples < 0) { // error handle
            logWarn("decode pcm failed: %d", numOfSamples);
        } else if (0 == numOfSamples) {
            logInfo("End of file reached or buffer is to small to store even one sample.");
            break;
        } else {
            // write pcm buffer to output
            // op_head(opusFile, currentLink);
            int ret = fwrite(buf, sizeof(opus_int16), numOfSamples, outFile);
            if (ret < numOfSamples) {
                logWarn("Wrote sample to file failed: %d", ret);
            }
#ifdef WRITE_DETAIL
            auto pcmPos = op_pcm_tell(opusFile);
            auto bitrate = op_bitrate_instant(opusFile);
            // test: ffplay -ar 48000 -ac channel -f s16le outPCMFile
//    using namespace std::chrono_literals;

            // sample rate is 48000, so 48 samples for one milliseconds
            auto sampleDuration = numOfSamples / (48 * channel);
            logDebug("Write %d sample, percentage=%.2lf%%, bitrate=%.1lf kbps, read sample duration=%d ms.",
                     ret, ((double) pcmPos) * 100 / pcmTotal, bitrate / 1000.0, sampleDuration);
            auto now = chrono::high_resolution_clock::now();
            std::chrono::duration<double, std::milli> elapsed = now - lastFrameGotTime;
//            auto needSleepSec = sampleDuration - elapsed;
//             sleep duration to match the speed or open Alsa playback in block mode
            this_thread::sleep_for(chrono::milliseconds(sampleDuration));
            lastFrameGotTime = now;
#endif
        }
    }
    op_free(opusFile);

    return 0;
}

ThirdPartLibOggAnalyzer::~ThirdPartLibOggAnalyzer() = default;
